package plp.orientadaObjetos2.expressao;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import plp.expressions2.memory.VariavelJaDeclaradaException;
import plp.expressions2.memory.VariavelNaoDeclaradaException;
import plp.orientadaObjetos1.comando.Comando;
import plp.orientadaObjetos1.declaracao.procedimento.DecParametro;
import plp.orientadaObjetos1.declaracao.procedimento.ListaDeclaracaoParametro;
import plp.orientadaObjetos1.excecao.declaracao.ClasseJaDeclaradaException;
import plp.orientadaObjetos1.excecao.declaracao.ClasseNaoDeclaradaException;
import plp.orientadaObjetos1.excecao.declaracao.ObjetoJaDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ObjetoNaoDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ProcedimentoJaDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ProcedimentoNaoDeclaradoException;
import plp.orientadaObjetos1.excecao.execucao.EntradaInvalidaException;
import plp.orientadaObjetos1.expressao.Expressao;
import plp.orientadaObjetos1.expressao.ListaExpressao;
import plp.orientadaObjetos1.expressao.leftExpression.Id;
import plp.orientadaObjetos1.expressao.valor.Valor;
import plp.orientadaObjetos1.expressao.valor.ValorBooleano;
import plp.orientadaObjetos1.expressao.valor.ValorInteiro;
import plp.orientadaObjetos1.expressao.valor.ValorNull;
import plp.orientadaObjetos1.expressao.valor.ValorRef;
import plp.orientadaObjetos1.expressao.valor.ValorString;
import plp.orientadaObjetos1.memoria.AmbienteCompilacaoOO1;
import plp.orientadaObjetos1.memoria.AmbienteExecucaoOO1;
import plp.orientadaObjetos1.memoria.AmbienteOO1;
import plp.orientadaObjetos1.memoria.colecao.ListaValor;
import plp.orientadaObjetos1.util.Tipo;
import plp.orientadaObjetos1.util.TipoPrimitivo;
import plp.orientadaObjetos2.ambiente.ExcecaoNativaException;
import plp.orientadaObjetos2.declaracao.funcao.DecFuncao;
import plp.orientadaObjetos2.declaracao.funcao.DecFuncaoConcreta;
import plp.orientadaObjetos2.declaracao.funcao.DecFuncaoNativa;
import plp.orientadaObjetos2.memoria.AmbienteCompilacaoOO2;
import plp.orientadaObjetos2.memoria.AmbienteExecucaoOO2;
import plp.orientadaObjetos2.memoria.ContextoExecucaoOO2;
import plp.orientadaObjetos2.memoria.DefClasseOO2;
import plp.orientadaObjetos2.memoria.FuncaoNaoDeclaradaException;
import plp.orientadaObjetos2.memoria.ObjetoNativo;
import plp.orientadaObjetos2.util.HierarquiaUtils;

public class ChamadaFuncao implements Expressao {

	/**
	 * Objeto que recebera a chamada a funcao
	 */
	private Expressao objetoAlvo;

	/**
	 * Nome da funcao que sera chamada
	 */
	private Id nomeFuncao;

	/**
	 * Lista de parametros reais desta funcao
	 */
	private ListaExpressao parametrosReais;

	public ChamadaFuncao(Expressao objetoAlvo, Id nomeFuncao,
			ListaExpressao parametrosReais) {
		this.objetoAlvo = objetoAlvo;
		this.nomeFuncao = nomeFuncao;
		this.parametrosReais = parametrosReais;
	}

	public Valor avaliar(AmbienteExecucaoOO1 ambiente)
			throws VariavelNaoDeclaradaException, VariavelJaDeclaradaException,
			ObjetoNaoDeclaradoException, ClasseNaoDeclaradaException {
		Valor retornoDaFuncao = null;
		ValorRef valorThis = (ValorRef) this.objetoAlvo.avaliar(ambiente);
		ListaValor parametrosAvaliados = this.parametrosReais.avaliar(ambiente);

		ambiente = new ContextoExecucaoOO2((AmbienteExecucaoOO2) ambiente);
		ambiente.incrementa();
		ambiente.map(new Id("this"), valorThis);
		
		Id idClasse = ambiente.getObjeto(valorThis).getClasse();
		DefClasseOO2 defClasse = (DefClasseOO2) ambiente.getDefClasse(idClasse);
		if(!defClasse.isNativa()){
			DecFuncaoConcreta funcao = (DecFuncaoConcreta) this.resolverFuncao(ambiente, defClasse);
			this.associaParametros(parametrosAvaliados, funcao, ambiente);
			
			Comando comando = funcao.getComando();
			if(comando != null){
				try {
					comando.executar(ambiente);
				} catch (ObjetoJaDeclaradoException e) {
					throw new RuntimeException("Excecao ao executar funcao " + funcao.getNome(), e);
				} catch (ProcedimentoNaoDeclaradoException e) {
					throw new RuntimeException("Excecao ao executar funcao " + funcao.getNome(), e);
				} catch (ProcedimentoJaDeclaradoException e) {
					throw new RuntimeException("Excecao ao executar funcao " + funcao.getNome(), e);
				} catch (ClasseJaDeclaradaException e) {
					throw new RuntimeException("Excecao ao executar funcao " + funcao.getNome(), e);
				} catch (EntradaInvalidaException e) {
					throw new RuntimeException("Excecao ao executar funcao " + funcao.getNome(), e);
				}
			}
			retornoDaFuncao = funcao.getExpressaoRetorno().avaliar(ambiente);
			ambiente.restaura();
		} else {
			retornoDaFuncao = despacharParaCodigoJava(valorThis, parametrosAvaliados, ambiente);
		}
		return retornoDaFuncao;
	}

	private Valor despacharParaCodigoJava(ValorRef valorThis,
			ListaValor parametrosAvaliados, AmbienteExecucaoOO1 ambiente) throws ObjetoNaoDeclaradoException {
		Valor retorno;
		AmbienteExecucaoOO2 ambienteOO2 = (AmbienteExecucaoOO2) ambiente;
		ObjetoNativo objeto = (ObjetoNativo)ambienteOO2.getObjeto(valorThis);
		Object objetoThis = objeto.getObjetoNativo();
		Method metodoJava = getMetodoJava(objetoThis);
		try {
			retorno = (Valor) metodoJava.invoke(objetoThis, ambienteOO2, parametrosAvaliados);
			Id classe = objeto.getClasse();
			DefClasseOO2 defClasse = (DefClasseOO2) ambiente.getDefClasse(classe);
			DecFuncao funcao = defClasse.getFuncao(this.nomeFuncao);
			Tipo tipoRetorno = funcao.getTipoRetorno();
			if(tipoRetorno.equals(TipoPrimitivo.TIPO_INTEIRO)){
				retorno = (ValorInteiro) retorno;
			} else if(tipoRetorno.equals(TipoPrimitivo.TIPO_STRING)){
				retorno = (ValorString) retorno;
			} else if(tipoRetorno.equals(TipoPrimitivo.TIPO_BOOLEANO)){
				retorno = (ValorBooleano) retorno;
			} else if(!(retorno instanceof ValorNull)) {
				retorno = (ValorRef) retorno;
			}
		} catch (IllegalAccessException e) {
			throw new ExcecaoNativaException(e);
		} catch (IllegalArgumentException e) {
			throw new ExcecaoNativaException(e);
		} catch (InvocationTargetException e) {
			throw new ExcecaoNativaException(e);
		} catch (ClasseNaoDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (FuncaoNaoDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		}
		return retorno;
	}

	private Method getMetodoJava(Object objetoThis) {
		Method metodoJava = null;
		Method[] methods = objetoThis.getClass().getMethods();
		for (Method method : methods) {
			if(method.getName().equals(this.nomeFuncao.getIdName())){
				metodoJava = method;
			}
		}
		return metodoJava;
	}

	private void associaParametros(ListaValor parametrosAvaliados,
			DecFuncaoConcreta funcao, AmbienteExecucaoOO1 ambiente) {
		ListaDeclaracaoParametro parametrosFormais = funcao.getParametrosFormais();
		while(parametrosAvaliados != null && parametrosAvaliados.getHead() != null){
			ambiente.map(parametrosFormais.getHead().getId(), parametrosAvaliados.getHead());
			parametrosAvaliados = (ListaValor) parametrosAvaliados.getTail();
			parametrosFormais = (ListaDeclaracaoParametro) parametrosFormais.getTail();
		}
	}

	public boolean checaTipo(AmbienteCompilacaoOO1 ambiente)
			throws VariavelNaoDeclaradaException, ClasseNaoDeclaradaException {
		boolean tipoOk = this.objetoAlvo.checaTipo(ambiente);
		tipoOk &= this.parametrosReais.checaTipo(ambiente);
		
		DefClasseOO2 defClasse = getDefClasseObjetoAlvo(ambiente);
		DecFuncao funcao = this.resolverFuncao(ambiente, defClasse);
		tipoOk &= funcao != null;

		if(funcao instanceof DecFuncaoConcreta ||
				(funcao instanceof DecFuncaoNativa && !((DecFuncaoNativa) funcao).hasVarargs())){
			ListaDeclaracaoParametro parametrosFormais = funcao.getParametrosFormais();
			tipoOk &= parametrosFormais.length() == this.parametrosReais.length();
	
			ListaExpressao parametrosReais = this.parametrosReais;
			while(parametrosFormais != null && parametrosFormais.getHead() != null && tipoOk){
				DecParametro parametroFormal = parametrosFormais.getHead();
				Expressao parametroReal = parametrosReais.getHead();
				tipoOk &= HierarquiaUtils.ehSubTipo(parametroReal.getTipo(ambiente),
													parametroFormal.getTipo(),
													(AmbienteCompilacaoOO2) ambiente) ||
							parametroFormal.getTipo().equals(parametroReal.getTipo(ambiente));
				
				parametrosFormais = (ListaDeclaracaoParametro) parametrosFormais.getTail();
				parametrosReais = (ListaExpressao) parametrosReais.getTail();
			}
		}
		return tipoOk;
	}

	private DefClasseOO2 getDefClasseObjetoAlvo(AmbienteCompilacaoOO1 ambiente)
			throws ClasseNaoDeclaradaException {
		return (DefClasseOO2) ambiente.getDefClasse(this.objetoAlvo.getTipo(ambiente).getTipo());
	}

	public Tipo getTipo(AmbienteCompilacaoOO1 ambiente)
			throws VariavelNaoDeclaradaException, ClasseNaoDeclaradaException {
		Tipo tipoDeRetorno = null;
		DefClasseOO2 defClasse = getDefClasseObjetoAlvo(ambiente);
		DecFuncao funcao = this.resolverFuncao(ambiente, defClasse);
		tipoDeRetorno = funcao.getTipoRetorno();
		return tipoDeRetorno;
	}

	private DecFuncao resolverFuncao(AmbienteOO1<?> ambiente, DefClasseOO2 defClasse) throws VariavelNaoDeclaradaException, ClasseNaoDeclaradaException{
		DecFuncao funcao = null;
		// Enquanto nao acharmos uma funcao com nomeFuncao ou nao atingirmos o topo da hierarquia
		// continue procurando...
		while(funcao == null && defClasse != null){
			try {
				funcao = defClasse.getFuncao(this.nomeFuncao);
			} catch (FuncaoNaoDeclaradaException e) {
				Id nomeSuperClasse = defClasse.getNomeSuperClasse();
				if(nomeSuperClasse != null){
					try{
						defClasse = (DefClasseOO2) ambiente.getDefClasse(nomeSuperClasse);
					} catch(ClasseNaoDeclaradaException cnde){
						defClasse = null;
					}
				} else {
					defClasse = null;
				}
			}
		}
		return funcao;
	}
}
